'''
Created on Jun 1, 2009

@author: mkiyer
'''

import sys
import os.path
import subprocess

class ApplicationError(OSError):
    pass

class CommandLineApplication(object):
    '''
    Generic class for controlling command line applications
    '''

    def __init__(self, stdout=None, stderr=None, cwd=None, 
                 options=None, args=None):
        '''
        Constructor
        
        stdout: file handle to redirect stdout
        stderr: file handle to redirect stderr
        cwd: path to working directory to run this app
        options: dict of command line options, for example:
            {'--verbose': None,
             '--size': 1024,
             '--name': 'a cool name'
             }
        args: a list of command-line arguments
        '''
        self._stdout = stdout
        self._stderr = stderr
        self.suppressStdOut = False
        self.suppressStdErr = False
        self._env = {}
        
        if not cwd:
            self.cwd = os.getcwd()
        else:
            self.cwd = cwd
        
        self._options = {}        
        if options:
            self._options = options
        self._args = []
        if args:
            self._args = args

    @property
    def executable(self):
        return self._executable
    
    #@property
    def cwd_get(self):
        return self._cwd
    #@cwd.setter
    def cwd_set(self, path):
        assert os.path.exists(path)
        self._cwd = os.path.abspath(path)
    cwd = property(cwd_get, cwd_set, None, "cwd property")

    # Functions for modifying OS environment variables
    def getenv(self):
        return self._env
    def setenv(self, k, v):
        self._env[k] = v

    #
    # Functions for modifying command line arguments
    #
    #@property
    def args_get(self):
        return self._args
    #@args.setter
    def args_set(self, args):
        self._args = args
    args = property(args_get, args_set, None, "args property")
    
    #
    # Functions for modifying command line options
    #
    def addOptions(self, options):
        self._options.update(options)
    def clearOptions(self):
        del self._options
        self._options = {}
    def addOption(self, key, value):
        self._options[key] = value
    def removeOption(self, key):
        del self._options[key]

    def getCommandLine(self):
        arglist = []
        # option arguments
        for k, v in self._options.iteritems():
            arglist.append(k)
            if v is not None:
                arglist.append(str(v))        
        # positional arguments
        arglist.extend(self._args)
        argstr = ' '.join(arglist)
        return ' '.join([self._executable, argstr])

    def _writeCommandLine(self, outfilename):
        # write command line to file
        cmd_file = os.path.join(os.path.abspath(self.cwd), outfilename)
        cmd_fhd = open(cmd_file, 'w')
        cmd_fhd.write(self.getCommandLine() + '\n')
        cmd_fhd.close()

    def get_result_paths(self):
        pass
    
    def start(self, args=None, dry_run=False):
        '''
        run the application and return a process descriptor
        '''        
        if not self._executable:
            raise ApplicationError, '_executable has not been set'

        #
        # output redirection
        #
        if self.suppressStdOut:
            # TODO: this is platform dependent code
            stdout = open(os.devnull, 'w')
        else:
            stdout = self._stdout
        
        if self.suppressStdErr:
            stderr = open(os.devnull, 'w')
        else:
            stderr = self._stderr
        
        if args:
            self._args = args

        # Environment modification
        app_env = None
        if self._env:
            # add app-specific environment variables            
            app_env = os.environ.copy()
            for k,v in self._env.iteritems():
                app_env[k] = v

        # TODO: use OptionParser to check command line args
        argstr = self.getCommandLine()

        try:
            p = subprocess.Popen(argstr, stdout=stdout, stderr=stderr, shell=True, cwd=self._cwd, env=app_env)
        except OSError, e:
            print >>sys.stderr, "Execution failed:", e
        return p
            
    def __call__(self, args=None, dry_run=False):
        '''
        run the application and wait for completion
        '''
        try:
            p = self.start(args, dry_run)
            if p is None:
                raise ApplicationError, 'Failed to execute process'
            retcode = p.wait()
            if retcode < 0:
                print >>sys.stderr, "Child was terminated by signal", -retcode
                return -retcode, None
            else:
                print >>sys.stderr, "Child returned", retcode
                return retcode, self.get_result_paths()
        except OSError, e:
            print >>sys.stderr, "Execution failed:", e
